<?php
/**
 * Created by PhpStorm.
 * User: s.utkin
 */

namespace Pinenut\Model2;

/**
 * Мультиязычная модель
 *
 * @package Pinenut
 */
abstract class MultilangModel extends \Model
{
	const MAP_TYPE_WHERE = 'Where';
	const MAP_TYPE_FIELDS = 'Fields';
	const MAP_TYPE_ORDER = 'Order';

	protected static $multilangFields;
	protected $_langId;

	/**
	 * Возвращает список полей, которые должны быть мультиязычными
	 *
	 * @return array
	 */
	protected static function multilangFields()
	{
	}

	/**
	 * @return mixed
	 */
	public function getLangId()
	{
		return $this->_langId;
	}

	/**
	 * @param mixed $langId
	 */
	public function setLangId($langId)
	{
		$this->_langId = $langId;
	}

	/**
	 * @return mixed
	 */
	protected static function getMultilangFields()
	{
		$className = get_called_class();

		if (!isset(static::$multilangFields[$className])) {
			static::$multilangFields[$className] = static::multilangFields();
		}

		return static::$multilangFields[$className];
	}

	/**
	 * @param $fieldName
	 * @param null $langId
	 * @return string
	 */
	protected static function getMappedFieldName($fieldName, $langId = null)
	{
		return '_' . $fieldName . ($langId ? '.' . $langId : '');
	}

	/**
	 * Возвращает карту мэппинга на основании кода языка и списка мультиязычных полей
	 * Мультиязычные поля строятся по формуле: _имяПоля.кодЯзыка
	 * Т.е. поле title преобразуется для русского языка в _title.ru
	 * Пример:
	 *        Язык: ru (русский)
	 *        На вход подается массив: [title, name]
	 *        На выходе будет массив:
	 *        [
	 *            'title' => '_title.ru',
	 *            'name' => '_name.ru'
	 *        ]
	 *
	 * @param array $fields - массив, содержащий имена полей, которые нужно мапить
	 * @param null $langId
	 * @param bool $reverse - возвращать массив, где ключем является мапленое поле, а значением - исходное
	 * @return array
	 */
	protected static function getMappedInfo($fields = null, $langId = null, $reverse = false)
	{
		$mappedInfo = [];
		$multilangFields = static::getMultilangFields();

		foreach ($fields as $field) {
			# если поля нет среди мультиязычных - пропускаем шаг
			if (empty($multilangFields) || !in_array($field, $multilangFields)) {
				continue;
			}

			$mappedField = static::getMappedFieldName($field, $langId);

			if (!$reverse) {
				$mappedInfo[$field] = $mappedField;
			} else {
				$mappedInfo[$mappedField] = $field;
			}
		}

		return $mappedInfo;
	}

	/**
	 * @param $fieldsInfo
	 * @param $type [where|fields|order]
	 * @param $langId
	 * @return mixed
	 */
	protected static function mapConditions($fieldsInfo, $type, $langId)
	{
		$fieldsInfoKeys = array_keys($fieldsInfo);
		$fields = array_keys($fieldsInfoKeys) !== $fieldsInfoKeys ? $fieldsInfoKeys : $fieldsInfo;

		$mapInfo = static::getMappedInfo($fields, $langId);

		foreach ($fields as $field) {
			if (!isset($mapInfo[$field])) {
				continue;
			}

			$handlerName = 'mapCondition' . $type;
			static::$handlerName($fieldsInfo, $mapInfo, $field);
		}

		return $fieldsInfo;
	}

	/**
	 * @param $fieldsInfo
	 * @param $mapInfo
	 * @param $field
	 */
	protected static function mapConditionWhere(&$fieldsInfo, $mapInfo, $field)
	{
		$fieldsInfo[$mapInfo[$field]] = $fieldsInfo[$field];
		unset($fieldsInfo[$field]);

		foreach ($fieldsInfo[$mapInfo[$field]] as $op => &$data) {
			$data['field'] = $mapInfo[$field];
		}
	}

	/**
	 * @param $fieldsInfo
	 * @param $mapInfo
	 * @param $field
	 */
	protected static function mapConditionOrder(&$fieldsInfo, $mapInfo, $field)
	{
		$fieldsInfo[$mapInfo[$field]] = $fieldsInfo[$field];
		unset($fieldsInfo[$field]);
	}

	/**
	 * @param $fieldsInfo
	 * @param $mapInfo
	 * @param $field
	 */
	protected static function mapConditionFields(&$fieldsInfo, $mapInfo, $field)
	{
		$index = array_search($field, $fieldsInfo);
		unset($fieldsInfo[$index]);
		$fieldsInfo[$field] = $mapInfo[$field];
	}

	/**
	 * @param Updater $updater
	 */
	protected static function multilangBehaviour(Updater &$updater)
	{
		$defaultLanguage = \MultilangConfig::getDefaultLanguageCode();
		$operators = $updater->getOperators();
		$modifiedFields = $operators['set'];

		$whereCondition = $updater->getWhere();
		if (isset($modifiedFields['_id']) && empty($whereCondition)) {
			$updater->where('_id', (string)$modifiedFields['_id']);
		}

		$whereCondition = $updater->getWhere();
		# Получаем raw-оригиналы по условию
		$results = static::getAdapter()->select($whereCondition);

		$multilangFields = static::getMultilangFields();
		$languageCodes = \MultilangConfig::getLanguageCodes();

		# перебираем измененные поля
		/** @var Updater[] $relatedUpdaters */
		$relatedUpdaters = [];
		foreach ($modifiedFields as $field => $modifiedValue) {
			$mappedField = explode('.', $field)[0];
			$mappedField = ltrim($mappedField, '_');

			# если модифицированного поля нет среди мультиязычных - пропускаем его
			if (!in_array($mappedField, $multilangFields)) {
				continue;
			}

			# проверяем, изменились ли поля, содержащие дефолтные значения
			foreach ($results as $id => $resultData) {
				if ($updater->isInsert() && !isset($resultData['_' . $mappedField][$defaultLanguage])) {
					$resultData['_' . $mappedField][$defaultLanguage] = '';
				}

				# если нет поля с дефолтным языком - пропускаем это поле
				if (!isset($resultData['_' . $mappedField][$defaultLanguage])) {
					continue;
				}

				# старое значение дефлотного языка
				$oldValue = $resultData['_' . $mappedField][$defaultLanguage];

				# перебираем языки, кроме дефлотного
				foreach ($languageCodes as $languageCode) {
					# игнорируем дефолтный язык
					if (!$updater->isInsert() && ($languageCode == $defaultLanguage)) {
						continue;
					}

					# если поле не совпадает по значению со старым дефолтным - пропускаем его.
					if (isset($resultData['_' . $mappedField][$languageCode]) && $resultData['_' . $mappedField][$languageCode] != $oldValue) {
						continue;
					}

					# формируем 'точечный' апдейтер
					if (!isset($relatedUpdaters[$id])) {
						$relatedUpdaters[$id] = static::update($id);
					}

					if ($updater->isInsert()) {
						$directLanguage = $updater->withLang() == $defaultLanguage;
						$relatedUpdaters[$id]->set("_$mappedField.$languageCode", $directLanguage ? $modifiedValue : '');
					} else {
						$relatedUpdaters[$id]->set("_$mappedField.$languageCode", $modifiedValue);
					}
				}
			}
		}

		foreach ($relatedUpdaters as $relatedUpdater) {
			$relatedUpdater->execute();
		}
	}

	/**
	 * @inheritdoc
	 */
	public static function find($id = null, $active = true, $langId = null)
	{
		$finder = new Finder(get_called_class());
		$finder->active($active);

		$finder->withLang($langId ?: \MultilangConfig::getDefaultLanguageCode());

		if (null === $id) {
			return $finder;
		}

		$finder->where('_id', $id);
		return is_array($id) ? $finder->all() : $finder->one();
	}

	/**
	 * @inheritdoc
	 */
	public function updaters($fields)
	{
		/** @var Updater[] $updaters */
		$updaters = parent::updaters($fields);
		$langId = $this->getLangId();

		if (empty($langId)) {
			$langId = \MultilangConfig::getDefaultLanguageCode();
		}

		foreach ($updaters as $updater) {
			$updater->withLang($langId);
		}

		return $updaters;
	}

	/**
	 * @inheritdoc
	 */
	public function getIndexes()
	{
		$multilangFields = static::multilangFields();
		$langCodes = \MultilangConfig::getLanguageCodes();

		$result = [];

		$indexes = $this->indexes();

		foreach ($indexes as $indexName => $index) {
			$fields = array_keys($index);
			if (empty(array_intersect($fields, $multilangFields))) {
				$result[$indexName] = $index;
				continue;
			}

			foreach ($langCodes as $langCode) {
				foreach ($index as $fieldName => $value) {
					if (!in_array($fieldName, $multilangFields)) {
						$result["{$indexName}_{$langCode}"][$fieldName] = $value;
						continue;
					}
					$result["{$indexName}_{$langCode}"]["_$fieldName.$langCode"] = $value;
				}
			}
		}

		return $result;
	}

	/**
	 * @inheritdoc
	 */
	protected static function onBeforeFetch(Finder &$finder)
	{
		$langId = $finder->withLang();

		if (empty($langId)) {
			$langId = \MultilangConfig::getDefaultLanguageCode();
		}

		$where = $finder->getWhere();
		$order = $finder->getOrder();
		$fields = $finder->getFields();

		if (empty($fields)) {
			$fields = array_keys(static::getFields());
		}

		$finder->setWhere(static::mapConditions($where, static::MAP_TYPE_WHERE, $langId));
		$finder->setOrder(static::mapConditions($order, static::MAP_TYPE_ORDER, $langId));
		$finder->setFields(static::mapConditions($fields, static::MAP_TYPE_FIELDS, $langId));

		parent::onBeforeFetch($finder);
	}

	/**
	 * @inheritdoc
	 */
	public static function onAfterFetchPopulate($model, Finder $finder)
	{
		/** @var static $model */
		$model->setLangId($finder->withLang());

		parent::onAfterFetchPopulate($model, $finder);
	}

	/**
	 * @inheritdoc
	 */
	protected static function onBeforeExecute(Updater &$updater)
	{
		$langId = $updater->withLang();
		/** @var Updater $updater */

		foreach ($updater->operators as &$operator) {
			$operator = static::mapConditions($operator, static::MAP_TYPE_ORDER, $langId);
		}

		# Если что-то вставляется, удаляется или изменения не на дефолтном языке - ничего не делаем
		$defaultLanguage = \MultilangConfig::getDefaultLanguageCode();
		if ($updater->isDelete() || $updater->isInsert() || ($defaultLanguage != $updater->withLang())) {
			parent::onBeforeExecute($updater);
			return;
		}

		# Если среди операторов нет set'ов - ничего не делаем
		$operators = $updater->getOperators();
		if (!isset($operators['set']) || empty($operators['set'])) {
			parent::onBeforeExecute($updater);
			return;
		}

		parent::onBeforeExecute($updater);
		self::multilangBehaviour($updater);
	}

	/**
	 * @inheritdoc
	 */
	protected static function onAfterExecute(Updater &$updater) {
		if (!$updater->isInsert()) {
			parent::onAfterExecute($updater);
			return;
		}

		static::multilangBehaviour($updater);
		parent::onAfterExecute($updater);
	}
} 